package com.sinevil.tcpTrans.utils.fileTrans.fileUpload;

import com.sinevil.tcpTrans.entity.json.FilePacketJson;
import com.sinevil.tcpTrans.entity.json.InstructionJson;
import com.sinevil.tcpTrans.tcp.TCPTransNode;
import com.sinevil.tcpTrans.utils.fileTrans.FileTranser;
import com.sinevil.tcpTrans.utils.fileTrans.entity.FilePacketRequestInfo;
import com.sinevil.tcpTrans.utils.fileTrans.entity.FileTask;
import com.sinevil.tcpTrans.utils.log.FileTransLogger;
import lombok.Getter;
import lombok.SneakyThrows;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Classname FileRequestManager
 * @Description TODO
 * @Version 1.0.0
 * @Date 2025/1/13 18:01
 * @Created by sinevil
 */
public final class FileUploadManager {
    private final TCPTransNode transNode;
    private final Thread UploadRequestProcessingThread = new Thread(new UploadRequestProcessingRunnable(),"UploadRequestProcessingThread");
    /**
     * 针对文件上传任务的锁
     */
    ReentrantLock TaskLocker = new ReentrantLock(true);
    /**
     * 正在执行的上传任务列表
     * (任务唯一标识：文件上传任务)
     */
    HashMap<String, FileUploadTask> uploadTaskMap = new HashMap<>();
    /**
     * (文件路径字符串：文件上传实体)
     * 用于存储待上传文件实体
     * 以实现通道多个客户端请求同一个文件时复用
     */
    Map<String,FileUploadEntity> uploadEntityMap = new HashMap<>();
    /**
     * 用于存储未处理的请求信息
     */
    LinkedBlockingQueue<FilePacketRequestInfo> unprocessedQueue = new LinkedBlockingQueue<>();

    /**
     * 待启动上传任务列表
     */
    LinkedBlockingQueue<FileUploadTask> awaitingUploadQueue  = new LinkedBlockingQueue<>();

    /**
     * 等待下载端回复的任务列表
     */
    Map<String, FileUploadTask> waitingForResponseTasks = new ConcurrentHashMap<>();

    /**
     * 失败的任务列表
     */
    List<FileUploadTask> failedUploadTasks = new ArrayList<>();

    public FileUploadManager(TCPTransNode transNode) {
        this.transNode = transNode;
        this.UploadRequestProcessingThread.start();
    }

    /**
     * 客户端请求下载文件，服务端添加上传任务
     * 并向客户端返回文件基本信息
     * @param socketChannel 客户端套接字
     * @param filePath 文件路径
     * @return 文件上传信息
     */
    public FileUploadInfo addUploadTask(SocketChannel socketChannel, String filePath){
        TaskLocker.lock();
        Path path = Path.of(filePath);
        try {
            // 检查文件是否存在
            if (path.toFile().exists()){
                // 检查此上传套接字映射是否存在相同路径的上传任务,若存在则直接返回文件上传信息
                for (FileUploadTask task : this.uploadTaskMap.values()) {
                    try {
                        if (task.sPath.equals(filePath) && task.socketChannel.equals(socketChannel)) {
                            FileTransLogger.LOGGER.info(" 已存在相同的上传任务 远程地址 {} {}", socketChannel.getRemoteAddress(), path);
                            this.transNode.sendMessage(socketChannel,new InstructionJson(InstructionJson.PUBLIC_FILE_INFO,task.getFileUploadInfo().toInstructionContent()));
                            return null;
                        }
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }

                // 若不存在则创建新的上传任务，并返回文件上传信息
                String uid = getRandomUID();
                FileUploadTask fileUploadTask = new FileUploadTask(socketChannel,uid,filePath);
                this.uploadTaskMap.put(uid,fileUploadTask);
                this.transNode.sendMessage(socketChannel,new InstructionJson(InstructionJson.PUBLIC_FILE_INFO,fileUploadTask.getFileUploadInfo().toInstructionContent()));

            } else {
                FileTransLogger.LOGGER.error("请求下载的文件不存在: {}", path);
                String response = String.format(InstructionJson.PUBLIC_UPLOAD_ERROR_REQUEST_FILE_FORMAT,path,"请求下载文件不存在");
                this.transNode.sendMessage(socketChannel,new InstructionJson(InstructionJson.PUBLIC_UPLOAD_ERROR_REQUEST_FILE,response));
                return null;
            }
        }
        finally {
            TaskLocker.unlock();
        }
        return null;
    }

    /**
     * 添加数据包请求
     * @param requestInfo 数据包请求
     * @return 是否成功添加请求
     */
    public boolean addUploadRequest(SocketChannel socketChannel, FilePacketRequestInfo requestInfo){
        try {
            // 如果对应上传任务存在
            if(uploadTaskMap.containsKey(requestInfo.uid)) {
                this.unprocessedQueue.put(requestInfo);
                return true;
            }
            // 如果此任务存在于 等待下载端回复的任务列表 ，则说明下载端已接受到上传请求，此时应将此任务移入 正在执行的上传任务列表
            else if (waitingForResponseTasks.containsKey(requestInfo.uid)) {
                FileUploadTask fileUploadTask = waitingForResponseTasks.get(requestInfo.uid);
                fileUploadTask.initFileUploadEntity();
                this.uploadTaskMap.put(requestInfo.uid,fileUploadTask);
                this.waitingForResponseTasks.remove(requestInfo.uid);
                this.unprocessedQueue.put(requestInfo);
                return true;
            } else {
                FileTransLogger.LOGGER.error("远程地址{} 请求不存在对应上传任务的数据包", socketChannel.getRemoteAddress());
                return false;
            }
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获取对应上传任务的MD5
     * @param uid 上传任务唯一标识
     * @return
     */
    public String getFileMD5(String uid){
        try{
            final FileUploadEntity uploadEntity = this.uploadTaskMap.get(uid).fileUploadEntity;
            if (uploadEntity.MD5 == null) {
                return uploadEntity.getMD5();
            }else
                return uploadEntity.MD5;
        }catch (Exception e){
            return null;
        }

    }

    /**
     * 完成文件上传任务
     * @param uid 上传任务唯一标识
     */
    public void completeUploadTask(String uid){
        TaskLocker.lock();
        try{
            if (uploadTaskMap.containsKey(uid)) {
                this.uploadTaskMap.get(uid).complete();
                this.uploadTaskMap.remove(uid);
            }
        }finally {
            TaskLocker.unlock();
        }
    }

    /**
     * 添加新的待执行上传任务
     * 此方法一般由客户端调用
     *
     * @param socketChannel 套接字
     * @param path          待上传文件路径
     */
    public void addPendingUploadTask(SocketChannel socketChannel, String path){
        FileUploadTask fileUploadTask = new FileUploadTask(socketChannel, getRandomUID(), path);;
        //
        if (this.waitingForResponseTasks.size() + this.uploadTaskMap.size() < FileTranser.MAX_TASK_NUM){
            this.transNode.sendMessage(socketChannel,new InstructionJson(InstructionJson.PUBLIC_FILE_INFO,fileUploadTask.getFileUploadInfo().toInstructionContent()));
            fileUploadTask.uploadRequestTime = System.currentTimeMillis();
            this.waitingForResponseTasks.put(fileUploadTask.uid,fileUploadTask);
        }else {
            this.awaitingUploadQueue.add(fileUploadTask);
        }
    }

    /**
     * 获取本次任务的唯一标识
     * UUID+时间戳
     * @return 唯一标识
     */
    private static String getRandomUID(){
        // 生成一个 UUID
         String uuid = UUID.randomUUID().toString();
        // 获取当前时间戳
         long timestamp = System.currentTimeMillis();
        // 将 UUID 和时间戳拼接在一起
         return uuid + "_" + timestamp;
    }


    /**
     * 处理数据包请求线程
     * 上传文件数据包和启动新的上传任务
     */
    class UploadRequestProcessingRunnable implements Runnable {
        protected final ByteBuffer uploadByteBuffer = ByteBuffer.allocate(FileTranser.PACKET_SIZE);

        @Override
        public void run() {
                handleUploadRequests();

        }

        private void handleUploadRequests(){
            int bytesRead;
            FileUploadTask uploadTask;
            FilePacketRequestInfo requestInfo;
            while (true) {
                // 使用 take 方法从队列中获取元素，如果队列为空则阻塞线程
                try {
                    requestInfo = unprocessedQueue.take();
                } catch (InterruptedException e) {
                    // 记录异常信息
                    FileTransLogger.LOGGER.info("Thread interrupted while waiting for request info", e);
                    // 继续循环
                    continue;
                }
                uploadTask = uploadTaskMap.get(requestInfo.uid);
                if (uploadTask != null) {
                    try {
                        bytesRead = uploadTask.getPacket(uploadByteBuffer, requestInfo.offset, requestInfo.size);
                        if (bytesRead > 0) {
                            // 将数据编码成base64格式的字符串
                            String data = FileTranser.Base64Encoder.encodeToString(uploadByteBuffer.array());
                            // 发送数据包
                            transNode.sendMessage(uploadTask.socketChannel, new FilePacketJson(uploadTask.uid, requestInfo.offset, bytesRead, data));
//                            System.out.printf("已处理请求: uid(%s) offset(%d)  size(%d)\n",requestInfo.uid,requestInfo.offset,requestInfo.size);
                        }
                    }catch (FileNotFoundException | NoSuchAlgorithmException e) {
                        throw new RuntimeException(e);
                    }catch (NullPointerException ignored) {
                        // 忽略空指针异常
                        System.out.println("空指针异常");
                    } catch (IOException ignored) {

                    }
                }

                if (awaitingUploadQueue.isEmpty() && waitingForResponseTasks.isEmpty()) {continue;}
                // 如果正在执行的下载任务数小于最大任务数
                else if (uploadTaskMap.size() < FileTranser.MAX_TASK_NUM) {
                    int needToStart = FileTranser.MAX_TASK_NUM - uploadTaskMap.size();
                    if (waitingForResponseTasks.size() >= needToStart) {
                        long now = System.currentTimeMillis();
                        long time_out = now - FileTranser.TIME_OUT;
                        // 遍历 等待下载端回复的任务列表， 检查是否由超时请求，如果有则重发
                        for (FileUploadTask fileUploadTask : waitingForResponseTasks.values()) {
                            // 检查重试次数
                            if(fileUploadTask.requestCount < FileTranser.MAX_RETRY_TIME){
                                // 如果上一次请求时间超过超时时间，则重发请求
                                if (fileUploadTask.uploadRequestTime < time_out) {
                                    fileUploadTask.uploadRequestTime = now;
                                    transNode.sendMessage(fileUploadTask.socketChannel,new InstructionJson(InstructionJson.PUBLIC_FILE_INFO,fileUploadTask.getFileUploadInfo().toInstructionContent()));
                                    fileUploadTask.requestCount ++;
                                }
                            }else {
                                // 将上传任务移入失败列表
                                failedUploadTasks.add(fileUploadTask);
                                waitingForResponseTasks.remove(fileUploadTask.uid);
                                // TODO 向外通知
                            }
                        }
                    }else {
                        while (!awaitingUploadQueue.isEmpty() && waitingForResponseTasks.size() < needToStart) {
                            FileUploadTask task = null;
                            task = awaitingUploadQueue.poll();
                            // 将待启动任务 移入 等待下载端回复的任务列表
                            if (task != null) {
                                waitingForResponseTasks.put(task.uid, task);
                                task.uploadRequestTime = System.currentTimeMillis();
                                transNode.sendMessage(task.socketChannel,new InstructionJson(InstructionJson.PUBLIC_FILE_INFO,task.getFileUploadInfo().toInstructionContent()));
                                task.requestCount++;
                            }
                        }
                    }
                }


            }
        }
    }

    /**
     * 文件上传实体
     * 用于维护文件通道和文件信息
     */
    class FileUploadEntity {
        public final String name;
        public final long size;
        /**
         * 文件路径的字符串形式
         */
        public final String sPath;
        @Getter
        protected String MD5 = null;
        /**
         * 记录有多少个任务在使用此文件
         * 当此值为0时，说明此文件已经没有任务在使用，可关闭回收资源
         */
        private int counter;
        private MessageDigest MD5Util = null;
        /**
         * 文件路径
         */
        private Path path;
        /**
         * 指向要上传的文件的随机访问对象
         */
        private RandomAccessFile raf = null;
        /**
         * 用于上传的文件通道
         */
        private FileChannel uploadChannel = null;
        /**
         * 用于获取MD5的文件通道
         */
        private FileChannel md5Channel = null;
        private long md5Offset = 0;

        protected FileUploadEntity(String path) {
            this.path = Path.of(path);
            this.sPath = path;
            File file = this.path.toFile();
            this.name = file.getName();
            this.size = file.length();
        }

        /**
         * 从给定的文件位置开始，读入指定大小的字节序列到给定缓冲区。
         * 获取数据包时顺便计算MD5
         *
         * @param byteBuffer 要传输字节的缓冲区
         * @param offset     给定的文件位置
         * @param size       要读取的字节数
         * @return 读取到的字节数，-1表示读取失败
         */
        public synchronized int getPacket(ByteBuffer byteBuffer, long offset, int size) throws IOException, NoSuchAlgorithmException, NullPointerException {
            if (this.raf == null) {
                this.raf = new RandomAccessFile(this.path.toFile(), "r");
                this.uploadChannel = raf.getChannel();
            }
            if (MD5 == null) {
                if (this.MD5Util == null) {
                    this.MD5Util = MessageDigest.getInstance("MD5");
                    this.md5Channel = raf.getChannel();
                    md5Offset = 0;
                }
                try {
                    md5Channel.position(md5Offset);
                    byteBuffer.clear();
                    md5Offset += md5Channel.read(byteBuffer, md5Offset);
                    byteBuffer.flip();
                    MD5Util.update(byteBuffer);
                    if (md5Offset >= this.size) {
                        MD5 = HexFormat.of().formatHex(MD5Util.digest());
                        md5Channel.close();
                        md5Channel = null;
                        MD5Util = null;
                    }
                } catch (ClosedChannelException e) {
                    md5Channel = raf.getChannel();
                    md5Offset = 0;
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

            try {
//                uploadChannel.position(offset);
                byteBuffer.clear();
                byteBuffer.limit(size);
                return uploadChannel.read(byteBuffer, offset);

            } catch (ClosedChannelException e) {
                this.raf.close();
                this.raf = new RandomAccessFile(this.path.toFile(), "r");
                this.uploadChannel = raf.getChannel();
                return uploadChannel.read(byteBuffer, offset);
            } catch (IOException e) {
                return -1;
            }
        }

        /**
         * 一个新的任务指向此文件实体
         */
        public void newTask() {
            counter++;
        }

        public void completeATask() {
            counter--;
        }

        /**
         * 如果下载端请求MD5时，MD5为null
         * 则通过此方法获取MD5
         *
         * @return MD5
         */
        @SneakyThrows
        public String getMD5() {
            if (MD5Util == null) {
                MD5Util = MessageDigest.getInstance("MD5");

            }
            if (md5Channel == null) {
                md5Channel = raf.getChannel();
                MD5Util.reset();
            }
            md5Offset = 0;
            // 10MB
            ByteBuffer md5Buffer = ByteBuffer.allocate(FileTranser.MB * 10);

            md5Channel.position(0);
            while (this.md5Channel.read(md5Buffer) != -1) {
                md5Buffer.flip();
                MD5Util.update(md5Buffer);
                md5Buffer.clear();
            }

            this.MD5 = HexFormat.of().formatHex(MD5Util.digest());
            return this.MD5;
        }

        /**
         * 当计数为0时，关闭文件实体
         *
         * @return 关闭是否成功，不成功则计数不为0，依然有任务在使用此文件实体
         */
        public boolean close() {
            if (counter <= 0) {
                try {
                    this.raf.close();
                    this.raf = null;
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
                return true;
            }
            return false;
        }


    }


    /**
     * 文件上传任务
     * 内部维护了上传文件基本信息和上传请求集合
     * 维护了已处理请求和未处理请求两个集合
     * 通过progressRequest标记此请求已被处理
     */
    class FileUploadTask extends FileTask {
        protected final String sPath;
        // 上传开始时间
        protected long time;
        protected FileUploadEntity fileUploadEntity;
        /**
         * 上传请求发出时间
         */
        protected long uploadRequestTime;
        /**
         * 记录尝试上传次数
         */
        protected int requestCount = 0;

        /**
         * 创建上传任务
         * 以上传文件基本信息作为参数创建
         *
         * @param socketChannel 套接字对象
         * @param uid           任务唯一标识
         */
        protected FileUploadTask(SocketChannel socketChannel, String uid, String path) {
            super(socketChannel, uid);
            this.sPath = path;
            this.time = System.currentTimeMillis();
            initFileUploadEntity();
        }

        protected void initFileUploadEntity() {
            if (uploadEntityMap.containsKey(sPath)) {
                this.fileUploadEntity = uploadEntityMap.get(sPath);
                this.fileUploadEntity.newTask();
            } else {
                this.fileUploadEntity = new FileUploadEntity(sPath);
                uploadEntityMap.put(sPath, fileUploadEntity);
            }
        }

        /**
         * 从给定的文件位置开始，读入指定大小的字节序列到给定缓冲区。
         *
         * @param byteBuffer 要传输字节的缓冲区
         * @param offset     给定的文件位置
         * @param size       要读取的字节数
         * @return 读取到的字节数
         */
        protected int getPacket(ByteBuffer byteBuffer, long offset, int size) throws IOException, NoSuchAlgorithmException {
            return fileUploadEntity.getPacket(byteBuffer, offset, size);
        }


        /**
         * 完成一个下载任务
         */
        protected void complete() {
            fileUploadEntity.completeATask();
            if (fileUploadEntity.close())
                uploadEntityMap.remove(sPath);
        }

        /**
         * 返回此任务的上传基本信息
         *
         * @return 任务的上传基本信息
         */
        public FileUploadInfo getFileUploadInfo() {
            return new FileUploadInfo(uid, fileUploadEntity.size, fileUploadEntity.name, this.sPath);
        }


    }
}